///////////////////////////////////////////////////////////////////////////////
// Copyright (c) 2009 Summit Contributors.  
// See: http://code.google.com/p/tyemill/wiki/SummitContributors
//
// The name and logo of Summit are trademarks of TYEMILL, LLC - All Rights 
// Reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a 
// copy of this software and associated documentation files (the "Software"), 
// to deal in the Software without restriction, including without 
// limitation the rights to use, copy, modify, merge, publish, distribute, 
// sublicense, and/or sell copies of the Software, and to permit persons to 
// whom the Software is furnished to do so, subject to the following 
// conditions:
//
// The above copyright notice and this permission notice shall be included 
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
// DEALINGS IN THE SOFTWARE.
///////////////////////////////////////////////////////////////////////////////

package summit.net
{
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.HTTPStatusEvent;
import flash.events.IOErrorEvent;
import flash.events.ProgressEvent;
import flash.events.SecurityErrorEvent;
import flash.net.URLLoader;
import flash.net.URLLoaderDataFormat;
import flash.net.URLRequest;
import flash.net.URLRequestHeader;

import summit.basecamp.constants.BasecampResource;
import summit.data.AccountToken;
import summit.events.BasecampAPIEvent;
import org.tyemill.utils.Assert;

/** Dispatched when the download operation commences following a call to 
 * the <code>load()</code>.
 * @eventType flash.events.Event.OPEN **/
[Event(name="open", type="flash.events.Event")]

/** Dispatched if a call to the <code>load()</code> method attempts to access 
 * data over HTTP, and Adobe AIR is able to detect and return the status code 
 * for the request.	
 * @eventType flash.events.HTTPStatusEvent.HTTP_RESPONSE_STATUS **/
[Event(name="httpResponseStatus", type="flash.events.HTTPStatusEvent")]

/** Dispatched if a call to <code>load()</code> attempts to access data over 
 * HTTP. 
 * @eventType flash.events.HTTPStatusEvent.HTTP_STATUS **/
[Event(name="httpStatus", type="flash.events.HTTPStatusEvent")]

/** Dispatched when data is received as the download operation progresses. 
 * @eventType flash.events.ProgressEvent.PROGRESS **/ 
[Event(name="progress", type="flash.events.ProgressEvent")]

/** Dispatched when an API call signals success. 
 * @eventType summit.events.BasecampAPIEvent.OK **/
[Event(name="ok", type="summit.events.BasecampAPIEvent")]

/** Dispatched when an API call does not signal success.
 * @eventType summit.events.BasecampAPIEvent.FAIL **/
[Event(name="fail", type="summit.events.BasecampAPIEvent")]

/** Dispatched when an API call's data is not valid XML or an internal
 * error occurs. 
 * @eventType summit.events.BasecampAPIEvent.ERROR **/
[Event(name="error", type="summit.events.BasecampAPIEvent")]

/** Dispatched if a call to <code>load()</code> results in a fatal error that 
 * terminates the download. 
 * @eventType flash.events.IOErrorEvent.IO_ERROR **/
[Event(name="ioError", type="flash.events.IOErrorEvent")]

/** Dispatched if a call to <code>.load()</code> attempts to load data from a 
 * server outside the security sandbox. 
 * @eventType flash.events.IOErrorEvent.SECURITY_ERROR **/
[Event(name="securityError", type="flash.events.SecurityErrorEvent")]

/** The <code>BasecampLoader</code> class is a wrapper for a 
 * <code>URLLoader</code> specifically tailored for calling the Basecamp API.
 * 
 * <p>Events and usage are generally the same as a standard 
 * <code>URLLoader</code>, however in place of the standard 
 * <code>COMPLETE</code> event, a <code>BasecampAPIEvent</code> is dispatched. 
 * The <code>BasecampAPIEvent</code> denotes the response from the Basecamp
 * API and provides easy access to API results.
 */ 
public class BasecampLoader extends EventDispatcher
{
	private static const DBGMSG0:String = 
	"event.status is OK, header has no information to confirm.";
	
	private static const DBGMSG1:String = 
	"event.status is OK, header data is not.";
	
	/** @private **/ // Property Storage
	private var _data:XML;
	private var _urlLoader:URLLoader;
	private var _turnOffStatusCheck:Boolean; 	// In some cases (so far only
												// only for attachments, 
												// responseHeaders are not
												// configured uniformly. In
												// this case, we turn off
												// deep status checks.
	
	private var _accountToken:AccountToken;
	private var _statusEvent:HTTPStatusEvent;
	private var _basecampRequest:BasecampRequest;
	
	/** The account token used to access the Basecamp account. **/
	public function get accountToken():AccountToken
	{
		return _accountToken;
	}
	/** @private **/
	public function set accountToken(value:AccountToken):void
	{
		_accountToken = value;
	}
	
	/** The <code>HTTPStatusEvent</code> issued during the HTTP exchange
	 * between Summit and the Basecamp API. **/
	public function get statusEvent():HTTPStatusEvent
	{
		return _statusEvent;
	}
	/** @private **/
	public function set statusEvent(value:HTTPStatusEvent):void
	{
		_statusEvent = value;
	}
	
	/** The request given to the loader. **/
	public function get basecampRequest():BasecampRequest
	{
		return _basecampRequest;
	}
	/** @private **/
	public function set basecampRequest(value:BasecampRequest):void
	{
		_basecampRequest = value;
	}
	
	/** The XML response given from the Basecamp API. **/
	public function get data():XML
	{
		return _data;
	}
	
	/** @private **/
	public function set data(value:XML):void
	{
		_data = value;
	}
	
	/** @private - used to clear internal when loader is used for multiple
	 * calls. **/
	private function cleanInternalState():void
	{
		this.statusEvent 			= null;
		this._data 					= null;
		this.statusEvent			= null;
		this.basecampRequest		= null;
		this._turnOffStatusCheck	= false;
		
		if(_urlLoader)
		{
			_urlLoader.bytesLoaded 	= 0;
			_urlLoader.bytesTotal 	= 0;
		}
	
	}
	
	/** Indicates the number of bytes that have been loaded thus far during 
	 * the load operation. 
	 * @default 0 **/
	public function get bytesLoaded():uint
	{
		if(!_urlLoader) return 0;
		else return _urlLoader.bytesLoaded;
	}
	
	/** Indicates the total number of bytes in the downloaded data. 
	 * @default 0 **/
	public function get bytesTotal():uint
	{
		if(!_urlLoader) return 0;
		else return _urlLoader.bytesTotal;
	}
	
	/**
	 * Cancels execution of this request.
	 */
	public function close():void
	{
		_urlLoader.close();
	}
	
	/**
	 * Makes a request to the Basecamp API based on the contents of the
	 * request.
	 * 
	 * @param request
	 * Parameters used to build the request to the Basecamp API.
	 */
	public function load(request:BasecampRequest):void
	{
		cleanInternalState();
		
		basecampRequest = request
		
		var urlRequest:URLRequest = basecampRequest.toURLRequest(accountToken); 
		
		if(!_urlLoader)
		{
			_urlLoader = new URLLoader();
			_urlLoader.dataFormat = URLLoaderDataFormat.TEXT;
			registerEvents(_urlLoader);
		}
		
		// Sadly, a special case for attachments.
		if(basecampRequest.resource == BasecampResource.ATTACHMENT) 
			_turnOffStatusCheck = true;
		
		if(basecampRequest.apiURL) _urlLoader.load(urlRequest);
		else simulateLoad(basecampRequest);
	}
	
	/** Constructor. **/
	public function BasecampLoader()
	{
		
	}
	
	/** @private Takes care of null operations - simulating loader behavior
	 * to enable uniform event handling / case management for usr code. **/
	private function simulateLoad(incomingRequest:BasecampRequest):void
	{
		var sEvent:HTTPStatusEvent = new HTTPStatusEvent(
		HTTPStatusEvent.HTTP_RESPONSE_STATUS, false, false, 200);
		
		sEvent.responseHeaders.push(new URLRequestHeader(
		"Status", "200 OK"));
		
		handleLoaderStatusEvent(sEvent);
		
		_urlLoader.data = incomingRequest.data;
		
		handleLoaderResponseEvent(null);
	}
	
	/** @private Handler for the HTTPStatusEvent **/
	private function handleLoaderStatusEvent(event:HTTPStatusEvent):void
	{
		statusEvent = event; 		// needed to check API status
									// Must do this here - as most 
		checkStatus(statusEvent); 	// non-success events are simply forwarded.
		
		var e:HTTPStatusEvent = event.clone() as HTTPStatusEvent;
		
		dispatchEvent(e);
	}
	
	/** @private Handler for the COMPLETE event **/
	private function handleLoaderResponseEvent(e:Event):void
	{
		var event:Event;
		
		if(checkStatus(statusEvent))
		{
			try
			{
				data = _urlLoader.data ? new XML(_urlLoader.data) : null;
				
				// If extra data should be inserted in the XML - add it
				// unless the XML already contains those elements. 
				// Typically used in data returned from prepare in
				// order to make calls to create easier for the developer.
				for(var prop:String in basecampRequest.xmlAdditions)
				{
					if(data.hasOwnProperty(prop)) continue;
					
					data.appendChild(new XML("<" + prop + "/>"));
					data.child(prop)[0] = basecampRequest.xmlAdditions[prop];
				}
				
				event = createAPIEvent(BasecampAPIEvent.OK);
				
				// If a request requires special parsing before dispatch...
				if(null != basecampRequest.callback)
				{
					(event as BasecampAPIEvent).loader = this;
					basecampRequest.callback(event);
				}
			}
			catch(err:Error)
			{
				event = createAPIEvent(BasecampAPIEvent.ERROR);
			}
		}
		else event = createAPIEvent(BasecampAPIEvent.FAIL);
		
		dispatchEvent(event);
	}
	
	/** @private Event Forwarding for common <code>URLLoader</code> 
	 * events. **/
	private function forwardLoaderEvents(e:Event):void
	{
		var newEvent:Event = e.clone();
		
		dispatchEvent(newEvent);
	}
	
	/** @private Helper for created <code>BasecampAPIEvents</code>. **/
	private function createAPIEvent(type:String):BasecampAPIEvent
	{
		return new BasecampAPIEvent(type, basecampRequest.resource,
		basecampRequest.operation, 
		statusEvent.responseHeaders[statusEvent.responseHeaders.length - 1], 
		data);
	}
	
	/** @private Helper for doing basic event registration. **/
	private function registerEvents(urlLoader:URLLoader):void
	{	
		urlLoader.addEventListener(Event.OPEN, 
			forwardLoaderEvents);
		urlLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, 
			forwardLoaderEvents);  
		urlLoader.addEventListener(ProgressEvent.PROGRESS, 
			forwardLoaderEvents);
		urlLoader.addEventListener(IOErrorEvent.IO_ERROR, 
			forwardLoaderEvents);
		urlLoader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, 
			forwardLoaderEvents); 
		
		urlLoader.addEventListener(HTTPStatusEvent.HTTP_RESPONSE_STATUS, 
			handleLoaderStatusEvent);
		urlLoader.addEventListener(Event.COMPLETE, 
			handleLoaderResponseEvent);	
	}
	
	/**
	 * @private
	 * 
	 * Checks to see if the HTTPStatusEvent provided by the server is OK.
	 * 
	 * <p>Originally, the event's <code>status</code> property was consulted as
	 * a direct authority as to the success of an API call. However, in some
	 * malformed cases (e.g. https://...grouphub.com/todo_lists/null.xml), 
	 * Flex / Basecamp has reported a <code>HTTPStatusEvent.status</code> of 
	 * 200 while actually representing a server error.</p>
	 * 
	 * <p>Given the discovered inconsistancies, this method takes the time to
	 * ensure the status code is actually represented in the header information
	 * provided with the status.</p>
	 * 
	 * <p><code>true</code> is ONLY returned if OK status is reflected in the
	 * header information.</p>
	 * 
	 * @return Boolean true if the response from Basecamp is in the OK range.
	 */
	private function checkStatus(event:HTTPStatusEvent):Boolean
	{
		if(!event) return false;
		
		var headers:Object
		
		// if parsed here before
		if(null != (headers = event.responseHeaders[
		event.responseHeaders.length - 1]) as URLRequestHeader)
		{
			headers  = new Object();
			
			for each(var header:URLRequestHeader in event.responseHeaders)
			{
				headers[header.name] = header.value;
			}
			
			event.responseHeaders.push(headers);
		}
		
		if(event.status < 200 || event.status >= 300) return false;
		
		// Normally, we check to make sure that the Flex reported status
		// is actually in the responseHeaders of the HTTPStatusEvent. In
		// all 'success' cases it is - accept - with attachments. With
		// attachments - Flex reports success - but the HTTPResponseHeaders
		// contain no actual successful reponse from the Basecamp API. 
		// However, even with no actual status in the headers, the API 
		// call is truly sucessful, so we've added a special case to 
		// let the lack of server status go unchecked.

		if(_turnOffStatusCheck == false)
		{
			if(!Assert.isTrue(
			headers.hasOwnProperty("Status"), DBGMSG0)) return false;
		
			if(!Assert.isTrue(
			headers.Status.charAt(0) == "2", DBGMSG1)) return false;
		}
		
		return true;
	}
}
}
//////////////////////////////////////////////////////// gmarius@tyemill.com //